package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Currency;

/**
 * 自定义的切面类,aspect就是切面的意思
 * 用来拦截@AutoFill注解标识的方法,目的是实现公共字段的自动填充
 * @author 岳海
 * @version 1.0
 * 312 / 356
 */
@Aspect    //加入这个注解,它才表示是一个切面类,要不只是普通的java类
@Component     //表示这是一个组件,可以注入到容器中,也是一个bean,所以要交给spring容器来管理
@Slf4j     //日志输出,方便调试
public class AutoFillAspect {
    //切面类怎么自定义,自己都忘光了

    //切面类首先要定义"切入点"和"通知"
    /**
     * 切入点
     * 就是对哪些类的哪些方法进行拦截
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    //@Pointcut表示这是一个切入点 ,   后面的对应的是切点表达式,不是正则表达式
        // 第一个*表示:  匹配所有返回类型
        //com.sky.mapper.* :   这指定了包名com.sky.mapper下的所有类
        //第二个".*" : 表示匹配所有的方法名
        //最后"(..)"  : 表示匹配所有的参数类型,,,就是传入的参数
            //看着不好理解,你想象包名.类名.方法名(参数名)  就好理解了
        //反正这行代码的总体意思:  匹配com.sky.mapper包下所有类的所有方法，不论这些方法的返回类型、方法名以及参数如何
    //你这把方法都拦截范围太大了,,所以后面条件,"且方法上标识有@AutoFill注解的方法才拦截"
    public void autoFillPointCut(){}


    /**
     * 前置通知
     * 在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")   //指定切入点,即当我们匹配上切点表达式的时候,就执行"前置通知"的方法
    public void autoFill(JoinPoint joinPoint){
        //传入的参数是JoinPoint,可以获取到哪个方法被拦截到了;包括被拦截的方法的参数值,参数类型

        //输出日志来进行测试
        log.info("开始进行公共字段的自动填充...");

        //为啥要设置下面的两个枚举属性,因为你update和insert要修改的值不一样
            //比如update语句,那么公共变量中,只需要修改updateTime以及updateUser属性
            //而insert语句,不仅要更改上面两个变量,还要额外修改createTime和createUser属性,
            //所以设置这个枚举类型,可以用来标记,

        //获取到当前被拦截的方法上的数据库操作类型
            //下面这部分就属于反射的内容;通过方法来获取到对应的类对象
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();//获取方法签名对象
            //向下转型,从Signature接口转换成子接口MethodSignature; 因为可能是要用到子接口的某个方法,父接口没有
        AutoFill autofill = signature.getMethod().getAnnotation(AutoFill.class);//获取方法上的注解对象
            //获取到对应方法上的注解对象;
        OperationType operationType = autofill.value();   //获取到数据库对应定义的操作类型,是update还是insert


        //获取到当前被拦截的方法的参数--就是实体对象
        Object[] args = joinPoint.getArgs();   //获取到所有的参数
        if(args == null || args.length == 0){   //说明这个args数组中就没有参数
            return;     //没有参数,就直接返回了
        }
            //下面取出第一个参数(老师说这里做了约定,要求就是取出第一个实体的参数);然后用对象接收一下
                //为啥取出第一个必须得是实体对象,因为填充公共字段需要对实体对象进行特定的处理;
                    //你一个普通的String参数,或者DTO对象属性可能不全,那就无法实现这个功能,,,
                    // 所以当前是基于这个填充公共字段的功能,才要取出第一个必须得是实体对象
        Object entity = args[0];
            //这里为啥用Object接收,而不是直接用Employee对象,因为将来你实体对象可能就不是Employee了,不能直接写死了
            //而且这种写法,能把当前AOP切面类变成一个通用的代码,什么类型就可以接收


        //准备赋值的数据---就是对获取到实体对象中的公共变量进行赋值,比如提到的updateTime以及updateUser等等
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据当前不同的操作类型,为对应的属性赋值,通过反射来赋值(ps:反射赋值也都忘光了)
        if (operationType == OperationType.INSERT){
            //如果是insert操作,就需要把四个公共字段都进行赋值
            //先获取到对应的set方法(通过反射来实现)
            try {
                //为了规范处理,把下面的字符串,放到了定义的常量类中,直接调用对应的常量类
                    //在sky-common对应的constant/AutoFillConstant.java
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                //问题1: 为啥上面要通过反射这种方式来获取到对应的set方法,为啥不能直接用对象实例来调用这个set方法?
                    //解答:1.我们可能不知道entity的具体类型是什么，或者我们想要编写一种更加通用的代码，能够处理多种不同类型的实体对象
                    //2.在实际开发中，尤其是在使用AOP时，我们往往面对的是泛型或未知类型的对象，这时就需要使用反射来动态地获取和调用方法。
                //问题2: 为啥要调用这个getClass()方法,不能直接创建实例,然后调用set方法吗??你这获取类对象是啥意思
                    //1. 这个entity上面我们是用Object类型来接收的,所以我们不知道entity这个对象实例实际的类型,
                        //所以这里可以通过反射,通过对象实例获取到具体的类对象,即class类型;
                //getDeclaredMethod("setCreateTime", LocalDateTime.class)
                    // 是方法名为"setCreateTime",后面的"LocalDateTime.class"表示方法的参数类型;最终获取到的是一个Method对象

                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值--------通过方法对象,反射到对应的对象
                setCreateTime.invoke(entity,now);  //对entity这个对象赋值,传入的实参值就是now,最终赋值给CreateUser这个属性
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                e.printStackTrace();
            }

        }else if (operationType == OperationType.UPDATE){
            //如果是update操作,就只需要赋值两个公共字段updateTime以及updateUser
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值--------通过方法对象,反射到对应的对象
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

}
